Skip to content

feat(angular-web-sdk): implement Angular Web SDK reference implementation#310

Closed
David Nalchevanidze (nalchevanidze) wants to merge 121 commits into
mainfrom
NT-3348
Closed

feat(angular-web-sdk): implement Angular Web SDK reference implementation#310
David Nalchevanidze (nalchevanidze) wants to merge 121 commits into
mainfrom
NT-3348

Conversation

@nalchevanidze

@nalchevanidze David Nalchevanidze (nalchevanidze) commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements a complete client-side SPA reference implementation of the Angular adapter SDK integration path using public APIs only, against shared mocks and fixtures. All behavior runs entirely on the client — no SSR, no server-side rendering, no hydration.

Constraint (REFREQ-20): No local shims, casts, or adapter logic mask a missing SDK capability. All SDK interaction goes through public package APIs.

What this implements

Core setup

  • SDK initialisation — initialises once per page load via a module-level singleton; falls back to baseline on failure; service cleans up subscriptions and calls sdk.destroy() on teardown
  • Multi-route navigation — Home (/) and Page Two (/page-two); page event fires on every SPA route change; SDK not re-initialised across navigations; event history preserved
  • Consent — toggle grants/withdraws tracking; page events always fire regardless of consent state
  • Identify and resetidentify() and reset() transition profile state; both persist across reloads via SDK storage
  • Preview panel — lazy-loaded on first open; forces live re-resolution while open; restores per-entry lock settings on close; supports audience and variant overrides
  • Locale consistency — Contentful client wrapped with sdk.withOptimizationLocale() before every fetch
  • Feature flags — subscribes to a 'boolean' flag via the SDK flag state API; auto-emits a flag-view event on access; returns undefined for anonymous sessions, resolves after identify()

Tracking

  • Analytics event log — renders the SDK eventStream in real time; view/hover rows update in place; events blocked by consent are absent; history persists across routes
  • Page tracking — fires on first load and every route change, regardless of consent
  • Attribute-based (auto)data-ctfl-* DOM attributes observed by the SDK; view, click, hover events fire after consent; stop on withdrawal. Click scenarios: direct, descendant, and ancestor all emit component_click
  • Code-based (manual) — entries registered via enableElement; emits view events only; no click or hover events

Live updates

  • Global toggle — OFF (default): entry resolves once then freezes; ON: re-resolves on every profile change
  • Per-entry overrides — Always live, Always locked, and Default modes implemented

Content

  • Entry resolution — SDK resolves the correct variant; always falls back to baseline on invalid data; no variant selection logic in app code
  • Nested entries — each level resolves independently; arbitrary nesting depth handled by the SDK without app-level recursion
  • Merge tags — profile-resolved values rendered inline; [Merge Tag] fallback when no profile is active
  • Rich text — renders as formatted HTML; merge tag nodes resolved inline with the same fallback behaviour

Files created

File Purpose
src/main.ts App entry point
src/app/app.ts + app.config.ts + app.routes.ts + app.html Root component, providers, routing
src/sdk/services/optimization.ts Singleton SDK service — init, page tracking, state observables
src/sdk/services/contentful-client.ts CDA client wrapped with sdk.withOptimizationLocale()
src/sdk/services/entry.ts Entry resolution, variant selection, live-update modes
src/sdk/config.ts + src/sdk/index.ts Angular DI config token and public SDK module exports
src/app/components/content-card/ Entry display components (badge, rich text, nested content)
src/app/components/control-panel/ Consent, identify/reset, live updates toggle, feature flag display
src/app/components/event-log/ Real-time analytics event sidebar
src/app/pages/home/ + pages/page-two/ Route pages
src/app/services/live-updates.ts Live-update global toggle service
src/app/fixtures.ts + src/app/types/ + src/app/utils.ts Entry IDs, types, type guards
angular.json, package.json, tsconfig.json, pnpm-workspace.yaml Build config, deps, workspace isolation
AGENTS.md, REQUIREMENTS.md, README.md Implementation rules, acceptance criteria, quick start

Notable decisions

  • Zoneless change detectionzone.js removed; uses provideZonelessChangeDetection with signals and toSignal() throughout
  • Standalone components — no NgModules; inject() for DI, input()/output() for component I/O, @if/@for control flow syntax
  • No file-role suffixeshome.ts not home.component.ts; Optimization not OptimizationService
  • Module-level singletoninstance and attachmentStarted prevent double-init; ngOnDestroy resets both for clean teardown
  • CSR only — no provideClientHydration, no SSR

🤖 Generated with Claude Code

Adds implementations/angular-web-sdk — an Angular 22 CSR skeleton that
serves a Hello World page and establishes the project structure for future
@contentful/optimization-web integration.

## What was added

- angular.json — Angular CLI build config (@angular/build:application),
  dev server on port 3000 (matching other implementations), production +
  development configurations, analytics disabled, packageManager set to pnpm
- package.json — Angular 22 deps, standard implementation scripts (dev,
  build, typecheck, clean, serve:mocks, launch), no zone.js (zoneless)
- tsconfig.json — single config (no tsconfig.app.json split; no tests yet),
  strict mode, ES2022 target, moduleResolution: bundler
- pnpm-workspace.yaml — sharedWorkspaceLockfile: false plus SDK tarball
  overrides so the implementation resolves local pkgs/ tarballs
- src/main.ts — bootstrapApplication(App, appConfig)
- src/app/app.ts — minimal standalone root component (Hello World)
- src/app/app.config.ts — provideBrowserGlobalErrorListeners,
  provideZonelessChangeDetection, provideRouter; no zone.js, no
  provideClientHydration (CSR only), no provideHttpClient (too early)
- src/app/app.routes.ts — empty Routes array
- src/index.html — HTML shell with <app-root>
- src/styles.css — minimal global reset
- scripts/launch-reference-app.sh — one-shot launcher: builds SDK pkgs,
  installs deps, starts mock server in background, starts Angular dev server
  in foreground, cleans up on exit
- AGENTS.md — local rules and commands
- README.md — standard repo header, quick start, manual setup, project
  structure, related links

## Key decisions

- Zoneless change detection (provideZonelessChangeDetection) — zone.js
  removed from deps and angular.json polyfills entirely
- Single tsconfig.json instead of the Angular CLI default split
  (tsconfig.json + tsconfig.app.json) — the split only pays off when a
  tsconfig.spec.json for tests is also present
- standalone: true omitted from App — redundant in Angular 19+, all
  components are standalone by default
- No SDK integration yet — added only when the public Angular surface is ready
- Root package.json gets implementation:angular-web-sdk shortcut matching
  the pattern of other implementations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Not present in other implementation READMEs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add CONFIG InjectionToken with hardcoded mock defaults (grows per feature)
- Wire app routes: / → HomeComponent, /page-two → PageTwoComponent
- Extract app root template to app.html, add nav links and router-outlet
- Add stub HomeComponent and PageTwoComponent
- Update REQUIREMENTS.md with feature-oriented progress table
- Remove .env.example (no longer needed with hardcoded defaults)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sses

- Rename home.component.ts → home.ts, class HomeComponent → Home
- Rename page-two.component.ts → page-two.ts, class PageTwoComponent → PageTwo
- Update app.routes.ts imports accordingly
- Add naming and modern Angular patterns rules to AGENTS.md
- Add implementation:angular-web-sdk shortcut to root package.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add design system to styles.css: nav, typography, card, entry grid, utility panel
- Add RouterLinkActive to nav with active class and exact match on home link

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add @contentful/optimization-web dependency (resolved via pnpm-workspace override)
- Add SDK config fields to CONFIG token (clientId, sdkEnvironment, urls, logLevel)
- Create Optimization service with module-level singleton and graceful error handling
- Wire page tracking via Router NavigationEnd — fires on every route change incl. initial load
- Inject Optimization in App root to force instantiation on startup
- Mark features 1 and 2 as done in REQUIREMENTS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(feature 18)

- Add contentful and @contentful/rich-text-types dependencies
- Add Contentful fields to CONFIG token (spaceId, token, environment, host, basePath)
- Create ContentfulClient service wrapping CDA with sdk.withOptimizationLocale()
- Add types/contentful.ts — typed entry skeleton and RichTextDocument
- Add utils/type-guards.ts — isRecord and isEntry helpers
- Mark feature 18 as done in REQUIREMENTS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…scenarios (features 3-6)

- Add OptimizationResolver service wrapping sdk.resolveOptimizedEntry with baseline fallback
- Add ContentEntry component with auto-tracking (data-ctfl-* attributes) and manual tracking
  (enableElement/clearElement via effect + OnDestroy) and all three click scenarios
  (direct/descendant/ancestor)
- Wire Home page to fetch all entries on init and pass selectedOptimizations down so entries
  re-resolve on profile changes
- Bridge SDK plain-function subscribe protocol to RxJS Observable for toSignal compatibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ual differentiation

- Add variant/baseline badge and green left-border accent to entry cards
- Restructure home page with page header, stat panel, and section headers
- Tune spacing: tighter entry-grid gap, larger section margins, bottom padding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ide (features 9-10)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eature 17)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(feature 15)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ew event (feature 19)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n REQUIREMENTS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rom public exports

Token is an implementation detail; provideContentfulOptimizationConfig
is the only public entry point consumers need.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…zationConfig from public exports

Neither is used outside the SDK itself.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…provideContentfulOptimizationConfig

provideContentfulOptimizationConfig now takes a plain object.
CONFIG injection token was a needless middleman between env vars
and the SDK config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…NTRIES to FIXTURES

Hardcoded entry IDs are seed data, not configuration. Remove the now-empty
config/ directory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ructuring

Destructuring import.meta.env breaks at runtime — Vite replaces
import.meta.env.FOO statically but does not define import.meta.env
as a real object.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… takeUntilDestroyed in EventLog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ose resolveMergeTag via NgContentfulEntry, wrap trackView on NgContentfulOptimization

- NgContentfulOptimizationResolver removed from public SDK index
- RichText now uses NgContentfulEntry.resolveMergeTag instead of injecting resolver directly
- NgContentfulOptimization.trackView() wraps sdk.trackView so page-two never touches raw sdk

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d guards

SDK construction is synchronous in Angular DI — a thrown error fails
bootstrap entirely, so sdk can never be undefined at runtime. Remove the
try/catch, error field, and all sdk === undefined fallback branches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…visibility

Remove trivial setConsent/identify/trackView/reset wrappers from NgContentfulOptimization —
callers use optimization.sdk directly, which is clearer in a reference implementation.
Move compound reset+page call into ControlPanel where it belongs as app-specific logic.

Mark isLive, lockSnapshot, clearSnapshot private on NgContentfulEntry; resolveEntry
private on NgContentfulOptimizationResolver; drop unused globalLiveUpdates$ observable.
Simplify NgContentfulClient by collapsing client+localizedClient into one resolvedClient field
and removing the stale sdk !== undefined guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… entry fetching

Use Angular resource() in Home and PageTwo to eliminate OnInit, manual loading signals,
and void async fire-and-forget. Extract FIXTURES.home.ids so the fetch ID list is defined
once in fixtures.ts rather than derived inline at call sites. Both pages now use a Map
for consistent entry lookup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Static methods are inaccessible from Angular templates; replace with a
protected readonly field pointing to FIXTURES.home.clickScenarios.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ce loader

Mirror the home page pattern: derive ids from module-level constants so
the fetch list is defined once and auto and manual stay single-ID references.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ding aliases

trackConversion was passed as a bare callback losing `this` context; arrow field
fixes the binding. Remove redundant loading signal aliases — templates reference
entries.isLoading() directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Never imported or used anywhere; resolveMergeTag on NgContentfulEntry
covers the use case directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…to NgContentfulEntry

The resolver was only ever used by NgContentfulEntry. Merge resolveEntry,
resolveMergeTag, and their helpers directly into NgContentfulEntry and
delete the resolver service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NgContentfulLiveUpdates is app-level UI plumbing, not an SDK concern.
Move it from src/sdk/services/ to src/app/ and remove it from the
sdk public index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ices, trim config

Move NgContentfulLiveUpdates to src/app/services/ where it belongs as app-level
UI plumbing. Remove tag/toggleSelector from NgContentfulOptimizationConfig — they
were app-specific DOM selectors with no SDK meaning. Drop the now-unused
togglePreviewPanel method and its helper. previewPanel.nonce stays in SDK config
as it is passed to the SDK panel attachment call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…entfulEntry

NgContentfulEntry no longer injects NgContentfulLiveUpdates. It accepts
a plain Signal<boolean> for liveUpdates via with(). ContentEntry computes
isLive (preview panel + global toggle + per-entry override) and passes it
down — matching the pattern of every other SDK implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rename title to framework-neutral, trim Prerequisites, collapse verbose
verification steps, and remove Angular/React implementation references.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Align REQUIREMENTS.md with jira ticket structure: rename entry resolution
and tracking sections, nest click scenarios under attribute-based tracking,
update consent wording. Add jira.md with categorised acceptance criteria.
Update home.html tracking section descriptions to clarify tracking-only scope.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion

Implement OnDestroy to unsubscribe the router subscription, call sdk.destroy()
to flush queues and stop entry interaction tracking, and reset the module-level
singleton so the SDK can be recreated cleanly if needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Preview panel overrides are handled entirely by the panel itself — no app
code needed. Blocked events are already absent from eventStream by design.
Remove all TODO markers and clarify explanations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ions

Reorganise into Core setup, Tracking, Entry resolution, Live updates, and
Content sections. Merge verification steps into description paragraphs.
Remove Prerequisites section and jira.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ine confirm steps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…atterns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@nalchevanidze David Nalchevanidze (nalchevanidze) changed the title feat(implementations): scaffold Angular Web SDK reference implementation feat(angular-web-sdk): implement Angular Web SDK reference implementation Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants